package com.simpou.commons.web.helper;

import com.simpou.commons.utils.reflection.Annotations;
import com.simpou.commons.utils.reflection.Generics;
import com.simpou.commons.utils.reflection.Reflections;
import com.simpou.commons.utils.string.StringConversorHelper;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * @author jonas1
 * @version 01/10/13
 */
public class FormParamsToPojoConversor<T> extends AbstractPojoAndFormParamsConversor<Map<String, List<String>>, T> {

    private final Class<T> clasz;

    public FormParamsToPojoConversor(final Class<T> clasz) {
        this(clasz, null);
    }

    public FormParamsToPojoConversor(final Class<T> clasz, final ConversorContext context) {
        super(context);
        this.clasz = clasz;
    }

    /**
     * @param multMap Mapa de parâmetros na sintaxe json usando mime url
     *                encoded.
     * @return Objeto resultante da conversão.
     * @throws FieldNotFoundException Campo não encontrado.
     * @throws InvalidValueException  Valor inválido para o tipo do requerido.
     * @throws ConversionException    Qualquer outro erro de conversão asociados a
     *                                erro de programação.
     * @throws Exception              Outras exceções não previstas.
     */
    @Override
    public T execute(final Map<String, List<String>> multMap) throws Exception {
        final MultiMapTree tree = new MultiMapTree(multMap);
        return execute(tree, false, null);
    }

    /**
     * @param tree
     * @param useConvertedValues
     * @param instance           Se instância null cria uma nova senão preenche a existente.
     * @return
     * @throws Exception
     */
    public T execute(final MultiMapTree tree,
                     final boolean useConvertedValues,
                     final T instance) throws Exception {
        final List<SetFieldInfo> visiteds = new ArrayList<SetFieldInfo>();
        final T result;
        if (instance == null) {
            result = execute(clasz, tree, visiteds, null, null, useConvertedValues);
        } else {
            result = execute(clasz, tree, visiteds, null, null, useConvertedValues, instance);
        }

        final Comparator<SetFieldInfo> infoComparator = new Comparator<SetFieldInfo>() {
            @Override
            public int compare(SetFieldInfo o1, SetFieldInfo o2) {
                return o1.item.getLevel().compareTo(o2.item.getLevel());
            }
        };

        final Map<Object, List<SetFieldInfo>> mapVisiteds = new HashMap<Object, List<SetFieldInfo>>();
        for (SetFieldInfo info : visiteds) {
            final Object key = info.value;
            final List<SetFieldInfo> value;
            if (mapVisiteds.containsKey(key)) {
                value = mapVisiteds.get(key);
            } else {
                value = new ArrayList<SetFieldInfo>();
                mapVisiteds.put(key, value);
            }
            value.add(info);
        }
        for (final List<SetFieldInfo> infos : mapVisiteds.values()) {
            if (infos.size() < 2) {
                continue;
            }
            // escolhe a referência do objeto de menor hierarquia na árvore, pois é o que
            // começa a recursão, logo todos seus campos será preenhido, ao contrário do objeto
            // recursivo que não terá preenchido os dados do objeto a qual pertence
            final SetFieldInfo mainInfo = Collections.min(infos, infoComparator);
            infos.remove(mainInfo);
            infos.remove(new SetFieldInfo(null, null, null, null));//objeto raiz

            for (final SetFieldInfo info : infos) {
                final Field field = info.field;
                if (Collection.class.isAssignableFrom(field.getType())) {
                    final Collection<Object> collection = (Collection<Object>) info.object;
                    final ArrayList<Object> auxList = new ArrayList<Object>(collection);
                    collection.clear();
                    for (Object item : auxList) {
                        if (mainInfo.value.equals(item)) {
                            collection.add(mainInfo.value);
                        } else {
                            collection.add(item);
                        }
                    }
                } else if (field.getType().isArray()) {
                    final Object[] array = (Object[]) info.object;
                    for (int i = 0; i < array.length; i++) {
                        Object item = array[i];
                        if (mainInfo.value.equals(item)) {
                            array[i] = mainInfo.value;
                        } else {
                            array[i] = item;
                        }
                    }
                } else {
                    field.set(info.object, mainInfo.value);
                }
            }
        }

        return result;
    }

    @RequiredArgsConstructor
    class SetFieldInfo {

        final Object object;

        final Field field;

        final Object value;

        final MultiMapTree item;

        @Override
        public boolean equals(Object obj) {
            return this.object == ((SetFieldInfo) obj).object;
        }

        @Override
        public int hashCode() {
            return 0;
        }
    }

    private <T> T execute(final Class<T> clasz,
                          final MultiMapTree tree,
                          final List<SetFieldInfo> visiteds,
                          final Object decObj,
                          final Field field,
                          final boolean useConvertedValues) throws Exception {
        final T instance = clasz.newInstance();
        return execute(clasz, tree, visiteds, decObj, field, useConvertedValues, instance);
    }

    private <T> T execute(final Class<T> clasz,
                          final MultiMapTree tree,
                          final List<SetFieldInfo> visiteds,
                          final Object decObj,
                          final Field field,
                          final boolean useConvertedValues,
                          final T instance) throws Exception {
        visiteds.add(new SetFieldInfo(decObj, field, instance, tree));
        final List<Field> fields = getFields(clasz, getContext());
        for (final MultiMapTreeItem child : tree) {
            execute(child, instance, fields, visiteds, useConvertedValues);
        }
        return instance;
    }

    private void execute(final MultiMapTreeItem item,
                         final Object object,
                         final List<Field> fields,
                         final List<SetFieldInfo> visiteds,
                         final boolean useConvertedValues) throws Exception {
        final String fieldName = item.getRoot();
        final Class<? extends Object> objClass = object.getClass();
        final Field field = getField(objClass, fieldName, fields);
        if (field == null) {
            if (getContext().isToIgnoreInvalidFields()) {
                return;
            } else {
                throw new FieldNotFoundException(objClass, fieldName);
            }
        } else {
            execute(item, object, field, true, visiteds, useConvertedValues);
        }
    }

    private void execute(final MultiMapTreeItem item,
                         final Object object,
                         final Field field,
                         final boolean processWrappers,
                         final List<SetFieldInfo> visiteds,
                         final boolean useConvertedValues) throws Exception {
        final boolean fieldAccessPriority = Annotations.isFieldAccessPriority(field);
        final Class<?> declaringClass = object.getClass();
        final Class<?> fieldType = Reflections.getFieldType(declaringClass, field);

        field.setAccessible(true);
        if (item instanceof MultiMapTreeLeaf) {
            final MultiMapTreeLeaf leaf = (MultiMapTreeLeaf) item;
            final Object fieldValue;
            if (leaf.getPath().endsWith("[]") ||
                    (useConvertedValues && leaf.getConvertedValue().size() > 1) ||
                    (!useConvertedValues && leaf.getValue().size() > 1)) {

                // detector de comportamentos estranhos do maven
                // em alguns casos geram tipos por exemplo: "[J" ou "[I"
                // executando a classe roda normalmente, o prob acontece durante build do maven
                if(fieldType.getName().length()<3){
                    return;
                }

                if (Collection.class.isAssignableFrom(fieldType)) {
                    // coleções de tipos básicos
                    final Collection<Object> collection = Reflections.newCollectionInstance(fieldType);
                    final Class<?> paramType = Generics.getParameterizedType(field.getGenericType());
                    if (useConvertedValues) {
                        for (final Object value : leaf.getConvertedValue()) {
                            collection.add(value);
                        }
                    } else {
                        for (final String value : leaf.getValue()) {
                            collection.add(StringConversorHelper.stringToType(paramType, value));
                        }
                    }
                    fieldValue = collection;
                } else {
                    // arrays de tipos básicos
                    final Class<?> componentType = fieldType.getComponentType();
                    int iterCount = 0;
                    final Object[] array;
                    if (useConvertedValues) {
                        array = Generics.newArray(componentType, leaf.getConvertedValue().size());
                        for (final Object value : leaf.getConvertedValue()) {
                            array[iterCount++] = value;
                        }
                    } else {
                        array = Generics.newArray(componentType, leaf.getValue().size());
                        for (final String value : leaf.getValue()) {
                            array[iterCount++] = StringConversorHelper.stringToType(componentType, value);
                        }
                    }
                    fieldValue = array;
                }
            } else {
                // tipos básicos
                if (useConvertedValues) {
                    fieldValue = leaf.getConvertedValue(0);
                } else {
                    fieldValue = getContext().unmarshal(declaringClass, field, leaf.getValue().get(0), fieldAccessPriority);
                }
            }
            //TODO está dando erros aleatorios para arrays de tipos básicos
            field.set(object, fieldValue);
        } else {
            final MultiMapTree tree = (MultiMapTree) item;
            for (final MultiMapTreeItem child : tree) {
                final Object instance;
                if (Collection.class.isAssignableFrom(fieldType)) {
                    // coleções de tipos complexos
                    final Collection<Object> collection = Reflections.newCollectionInstance(fieldType);
                    final Class<?> paramType = Generics.getParameterizedType(field.getGenericType());

                    final Map<Class<?>, Map<String, String>> wrappers = getContext().getWrappers();

                    final boolean isParamTypeWrapped = processWrappers &&
                            wrappers != null &&
                            wrappers.containsKey(declaringClass) &&
                            wrappers.get(declaringClass).containsKey(field.getName());

                    final boolean notWrapped = !processWrappers ||
                            Annotations.getPojoFieldAnnotation(field, XmlElementWrapper.class, fieldAccessPriority) == null;

                    if (isParamTypeWrapped) {
                        final String wrapperName = wrappers.get(declaringClass).get(field.getName());
                        final MultiMapTreeItem singleChild = tree.getSingleChild();
                        if (singleChild.getRoot().equals(wrapperName)) {
                            execute(singleChild, object, field, false, visiteds, useConvertedValues);
                        }
                        continue;
                    } else if (notWrapped) {
                        // coleções de tipos complexos
                        for (final Iterator<MultiMapTreeItem> it = tree.sortedIterator(); it.hasNext(); ) {
                            final Object result;
                            final MultiMapTreeItem next = it.next();
                            if (next instanceof MultiMapTree) {
                                final MultiMapTree childTree = (MultiMapTree) next;
                                result = execute(paramType, childTree, visiteds, collection, field, useConvertedValues);
                            } else{
                                result = ((MultiMapTreeLeaf) next).getConvertedValue(0);
                            }
                            collection.add(result);
                        }
                        instance = collection;
                    } else {
                        // coleções de tipos básicos anotadas com wrapper
                        XmlElement annot;
                        try {
                            annot = Annotations.getPojoFieldAnnotation(field, XmlElement.class, fieldAccessPriority);
                        } catch (Exception e) {
                            annot = null;
                        }
                        //TODO só foi testado suporte a listas com elementos customizados e wrappers, se tiver só uma das duas anotação não é garatido q vá funcionar
                        final MultiMapTreeItem singleChild = tree.getSingleChild();
                        if ((annot == null && singleChild.getRoot().equals(field.getName()))
                                || (annot != null && singleChild.getRoot().equals(annot.name()))) {
                            execute(singleChild, object, field, false, visiteds, useConvertedValues);
                        }
                        continue;
                    }
                } else if (fieldType.isArray()) {
                    // arrays de tipos complexos
                    final Class<?> componentType = fieldType.getComponentType();
                    final Object[] array = Generics.newArray(componentType, tree.countChilds());
                    for (final MultiMapTreeItem arrayChild : tree) {
                        final Object newInstance;
                        if (useConvertedValues && arrayChild instanceof MultiMapTreeLeaf) {
                            newInstance = ((MultiMapTreeLeaf) arrayChild).getConvertedValue(0);
                        } else {
                            final MultiMapTree childTree = (MultiMapTree) arrayChild;
                            newInstance = execute(componentType, childTree, visiteds, array, field, useConvertedValues);
                        }
                        array[Integer.valueOf(arrayChild.getRoot())] = newInstance;
                    }
                    instance = array;
                } else {
                    // tipos complexos
                    if (child instanceof MultiMapTreeLeaf && useConvertedValues) {
                        instance = ((MultiMapTreeLeaf) child).getConvertedValue().get(0);
                    } else {
                        final Object curInstance = field.get(object);
                        if (curInstance == null) {
                            instance = fieldType.newInstance();
                            visiteds.add(new SetFieldInfo(object, field, instance, tree));
                        } else {
                            instance = curInstance;
                        }
                        final List<Field> allFields = getFields(fieldType, getContext());
                        execute(child, instance, allFields, visiteds, useConvertedValues);
                    }
                }

                field.set(object, instance);
            }
        }
    }

    private Field getField(final Class<?> clasz, final String fieldName, final List<Field> fields) {
        Field field = Reflections.getField(clasz, fieldName);
        final boolean fieldAccessPriority = clasz.isAnnotationPresent(XmlAccessorType.class) &&
                clasz.getAnnotation(XmlAccessorType.class).value().equals(XmlAccessType.FIELD);
        if (field == null) {
            for (Field fieldItem : fields) {
                final Class<?> type = Reflections.getFieldType(clasz, fieldItem);
                if (type.isArray() || Collection.class.isAssignableFrom(type)) {
                    final XmlElementWrapper annot;
                    try {
                        annot = Annotations.getPojoFieldAnnotation(fieldItem, XmlElementWrapper.class, fieldAccessPriority);
                    } catch (Exception e) {
                        continue;
                    }
                    if (annot != null) {
                        if (annot.name().equals(fieldName)) {
                            field = fieldItem;
                            break;
                        }
                    }
                } else {
                    final XmlAttribute annot;
                    try {
                        annot = Annotations.getPojoFieldAnnotation(fieldItem, XmlAttribute.class, fieldAccessPriority);
                    } catch (Exception e) {
                        continue;
                    }
                    if (annot != null) {
                        if ((getContext().getAttPreffix() + annot.name()).equals(fieldName)
                                || (getContext().getAttPreffix() + fieldItem.getName()).equals(fieldName)) {
                            field = fieldItem;
                            break;
                        }
                    }
                }
            }
        }
        return field;
    }

    @Override
    public void doOnError(final Map<String, List<String>> object, final Throwable throwable) {
        throw new RuntimeException(throwable);
    }

    public static interface MultiMapTreeItem extends Comparable<MultiMapTreeItem> {

        String getRoot();
    }

    @RequiredArgsConstructor
    public static class MultiMapTreeLeaf implements MultiMapTreeItem {

        @Getter
        private final String root;

        @Getter
        private final List<String> value;

        @Getter
        private final String path;

        @Getter
        @Setter
        private List<Object> convertedValue = new ArrayList<Object>();

        private Object getValue(final int index) {
            if (value == null || index >= value.size()) {
                throw new IllegalArgumentException("Expected element on position " + index + " at values of param " + path);
            } else {
                return value.get(index);
            }
        }

        private Object getConvertedValue(final int index) {
            if (convertedValue == null || index >= convertedValue.size()) {
                throw new IllegalArgumentException("Expected element on position " + index + " at converted values of param " + path);
            } else {
                return convertedValue.get(index);
            }
        }

        @Override
        public int compareTo(MultiMapTreeItem o) {
            return root.compareTo(o.getRoot());
        }
    }

    public static class MultiMapTreeLeafIterator implements Iterator<MultiMapTreeLeaf> {

        private final Deque<Iterator<MultiMapTreeItem>> iterators = new ArrayDeque<Iterator<MultiMapTreeItem>>();

        private Iterator<MultiMapTreeItem> curIterator;

        public MultiMapTreeLeafIterator(final MultiMapTree tree) {
            this.curIterator = tree.iterator();
        }

        @Override
        public boolean hasNext() {
            if (curIterator.hasNext()) {
                return true;
            } else {
                if (iterators.size() > 0) {
                    curIterator = iterators.poll();
                    return hasNext();
                } else {
                    return false;
                }
            }
        }

        @Override
        public MultiMapTreeLeaf next() {
            final MultiMapTreeItem next = curIterator.next();
            if (next instanceof MultiMapTree) {
                iterators.offer(((MultiMapTree) next).iterator());
            }
            if (next instanceof MultiMapTreeLeaf) {
                return (MultiMapTreeLeaf) next;
            } else if (hasNext()) {
                return next();
            } else {
                throw new IllegalStateException("No leafs found.");
            }
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    public static class MultiMapTree implements MultiMapTreeItem, Iterable<MultiMapTreeItem> {

        @Getter
        private final String root;

        @Getter
        private final Integer level;

        private final Map<String, MultiMapTreeItem> childs = new HashMap<String, MultiMapTreeItem>();

        private static void process(final MultiMapTree tree,
                                    final StringTokenizer tokenizer,
                                    final List<String> value,
                                    final int curLevel,
                                    final String fullPath) {
            final int level = curLevel + 1;
            final String root = tokenizer.nextToken();
            if (tokenizer.hasMoreElements()) {
                final MultiMapTree childTree;
                if (tree.childs.containsKey(root)) {
                    final MultiMapTreeItem treeItem = tree.childs.get(root);
                    if (treeItem instanceof MultiMapTree) {
                        childTree = (MultiMapTree) treeItem;
                    } else {
                        tree.childs.put(root, treeItem);
                        return;
                    }
                } else {
                    childTree = new MultiMapTree(root, level);
                    tree.childs.put(root, childTree);
                }
                process(childTree, tokenizer, value, level, fullPath);
            } else {
                tree.childs.put(root, new MultiMapTreeLeaf(root, value, fullPath));
            }
        }

        private MultiMapTree(final String root, final int level) {
            this.root = root;
            this.level = level;
        }

        public MultiMapTree(final Map<String, List<String>> multMap) {
            this.root = null;
            this.level = 0;
            // ordenar é importante na detecção de loops de referências nos objetos pois
            // força que os objetos menos profundos sejam processados primeiro
            final List<String> sortedKeyList = new ArrayList<String>(multMap.keySet());
            Collections.sort(sortedKeyList);
            for (final String key : sortedKeyList) {
                final List<String> value = multMap.get(key);
                final StringTokenizer tokenizer = new StringTokenizer(key, "[]");
                MultiMapTree.process(this, tokenizer, value, this.level, key);
            }
        }

        int countChilds() {
            return childs.size();
        }

        //TODO método com design muito ruim
        public MultiMapTreeItem getSingleChild() {
            final Collection<MultiMapTreeItem> values = childs.values();
            MultiMapTreeItem value = null;
            if (values.size() != 1) {
                throw new IllegalStateException("Multiple childs. Use iterator.");
            } else {
                for (MultiMapTreeItem item : values) {
                    value = item;
                    break;
                }
            }
            return value;
        }

        public Iterator<MultiMapTreeItem> sortedIterator() {
            List<MultiMapTreeItem> list = new ArrayList<MultiMapTreeItem>(childs.values());
            Collections.sort(list);
            return list.iterator();
        }

        @Override
        public Iterator<MultiMapTreeItem> iterator() {
            return childs.values().iterator();
        }

        @Override
        public int compareTo(MultiMapTreeItem o) {
            return root.compareTo(o.getRoot());
        }
    }
}
